﻿using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace Html5Canvas
{
    // Implements the CanvasRenderingContext2D interface
    [ScriptableType]
    public class CanvasRenderingContext2D
    {
        private Canvas _root;
        private Canvas _canvas;
        private Brush _fill;
        private Brush _stroke;
        private PathGeometry _pathGeometry;
        private bool _hasSubPaths;
        private TransformGroup _transformGroup = new TransformGroup();
        private Stack<DrawingState> _drawingStates = new Stack<DrawingState>();

        public CanvasRenderingContext2D(Canvas root)
        {
            // Initialize
            _root = root;
            _canvas = new Canvas { Width = _root.Width, Height = _root.Height, RenderTransform = _transformGroup };
            _root.Children.Add(_canvas);
            globalAlpha = 1.0;
            fillStyle = "#000000";
            strokeStyle = "#000000";
            lineWidth = 1;
            lineCap = "butt";
            lineJoin = "miter";
            miterLimit = 10;
            beginPath();

            // Handle size change by updating children
            ((FrameworkElement)(Application.Current.RootVisual)).SizeChanged += delegate(object sender, SizeChangedEventArgs e)
            {
                _root.Width = e.NewSize.Width;
                _root.Height = e.NewSize.Height;
                foreach (var child in _root.Children.OfType<FrameworkElement>())
                {
                    child.Width = _root.Width;
                    child.Height = _root.Height;
                }
            };
        }

        public void save()
        {
            // Save state on stack
            _drawingStates.Push(new DrawingState
            {
                strokeStyle = strokeStyle,
                fillStyle = fillStyle,
                globalAlpha = globalAlpha,
                lineWidth = lineWidth,
                lineCap = lineCap,
                lineJoin = lineJoin,
                miterLimit = miterLimit,
                transformGroup = _transformGroup,
            });
        }

        public void restore()
        {
            // Restore state from stack
            if (_drawingStates.Any())
            {
                var drawingState = _drawingStates.Pop();
                strokeStyle = drawingState.strokeStyle;
                fillStyle = drawingState.fillStyle;
                globalAlpha = drawingState.globalAlpha;
                lineWidth = drawingState.lineWidth;
                lineCap = drawingState.lineCap;
                lineJoin = drawingState.lineJoin;
                miterLimit = drawingState.miterLimit;

                // Create a new Canvas for the transform
                _transformGroup = drawingState.transformGroup;
                _canvas = new Canvas { Width = _root.Width, Height = _root.Height, RenderTransform = _transformGroup };
                _root.Children.Add(_canvas);
            }
        }

        public void scale(double x, double y)
        {
            // Create new Canvas for the new combined transform
            _transformGroup = Utility.CloneTransformGroup(_transformGroup);
            _transformGroup.Children.Insert(0, new ScaleTransform { ScaleX = x, ScaleY = y });
            _canvas = new Canvas { Width = _root.Width, Height = _root.Height, RenderTransform = _transformGroup };
            _root.Children.Add(_canvas);
        }

        public void rotate(double angle)
        {
            // Create new Canvas for the new combined transform
            _transformGroup = Utility.CloneTransformGroup(_transformGroup);
            _transformGroup.Children.Insert(0, new RotateTransform { Angle = (angle / (2 * Math.PI)) * 360 });
            _canvas = new Canvas { Width = _root.Width, Height = _root.Height, RenderTransform = _transformGroup };
            _root.Children.Add(_canvas);
        }

        public void translate(double x, double y)
        {
            // Create new Canvas for the new combined transform
            _transformGroup = Utility.CloneTransformGroup(_transformGroup);
            _transformGroup.Children.Insert(0, new TranslateTransform { X = x, Y = y });
            _canvas = new Canvas { Width = _root.Width, Height = _root.Height, RenderTransform = _transformGroup };
            _root.Children.Add(_canvas);
        }

        public double globalAlpha
        {
            get { return _globalAlpha; }
            set
            {
                if ((0.0 <= value) && (value <= 1.0))
                {
                    _globalAlpha = value;
                }
            }
        }
        private double _globalAlpha;

        public object strokeStyle
        {
            get { return _strokeStyle; }
            set
            {
                var stringValue = value as string;
                var gradientValue = value as CanvasGradient;
                var patternValue = value as CanvasPattern;
                if (null != stringValue)
                {
                    // Parse string style
                    Brush brush;
                    string style;
                    if (Utility.ParseStyle(stringValue, out brush, out style))
                    {
                        _stroke = brush;
                        _strokeStyle = style;
                    }
                }
                else if (null != gradientValue)
                {
                    // Apply gradient style
                    _stroke = gradientValue.GradientBrush;
                    _strokeStyle = value;
                }
                else if (null != patternValue)
                {
                    // Apply pattern style
                    _stroke = patternValue.ImageBrush;
                    _strokeStyle = value;
                }
            }
        }
        private object _strokeStyle;

        public object fillStyle
        {
            get { return _fillStyle; }
            set
            {
                var stringValue = value as string;
                var gradientValue = value as CanvasGradient;
                var patternValue = value as CanvasPattern;
                if (null != stringValue)
                {
                    // Parse string style
                    Brush brush;
                    string style;
                    if (Utility.ParseStyle(stringValue, out brush, out style))
                    {
                        _fill = brush;
                        _fillStyle = style;
                    }
                }
                else if (null != gradientValue)
                {
                    // Apply gradient style
                    _fill = gradientValue.GradientBrush;
                    _fillStyle = value;
                }
                else if (null != patternValue)
                {
                    // Apply pattern style
                    _fill = patternValue.ImageBrush;
                    _fillStyle = value;
                }
            }
        }
        private object _fillStyle;

        [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Making this static breaks the HTML Bridge interface for this method.")]
        public CanvasGradient createLinearGradient(double x0, double y0, double x1, double y1)
        {
            // Translate coordinates
            var brush = new LinearGradientBrush { StartPoint = new Point(x0, y0), EndPoint = new Point(x1, y1), MappingMode = BrushMappingMode.Absolute };
            return new CanvasGradient { GradientBrush = brush };
        }

        [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Making this static breaks the HTML Bridge interface for this method.")]
        public CanvasGradient createRadialGradient(double x0, double y0, double r0, double x1, double y1, double r1)
        {
            // Translate coordinates
            var brush = new RadialGradientBrush { GradientOrigin = new Point(x0, y0), Center = new Point(x1, y1), RadiusX = r1, RadiusY = r1, MappingMode = BrushMappingMode.Absolute };
            var rMin = Math.Min(r0, r1);
            var rMax = Math.Max(r0, r1);
            return new CanvasGradient { GradientBrush = brush, OffsetMinimum = rMin / rMax, OffsetMultiplier = (rMax - rMin) / rMax };
        }

        [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Data is in string form.")]
        [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "repetition", Justification = "Public API exposes this (unused) parameter.")]
        [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Making this static breaks the HTML Bridge interface for this method.")]
        public CanvasPattern createPattern(HtmlElement image, string repetition)
        {
            var brush = new ImageBrush { ImageSource = new BitmapImage { UriSource = new Uri(HtmlPage.Document.DocumentUri, image.GetAttribute("src")) }, Stretch = Stretch.None };
            return new CanvasPattern { ImageBrush = brush };
        }

        public double lineWidth
        {
            get { return _lineWidth; }
            set
            {
                if ((0.0 < value) && !double.IsInfinity(value))
                {
                    _lineWidth = value;
                }
            }
        }
        private double _lineWidth;

        public string lineCap
        {
            get { return _lineCap.ToString(); }
            set
            {
                try
                {
                    _lineCap = (CapTypes)Enum.Parse(typeof(CapTypes), value, true);
                }
                catch (ArgumentException)
                {
                    // Ignore bogus value
                }
            }
        }
        private CapTypes _lineCap;

        public string lineJoin
        {
            get { return _lineJoin.ToString(); }
            set
            {
                try
                {
                    _lineJoin = (JoinTypes)Enum.Parse(typeof(JoinTypes), value, true);
                }
                catch (ArgumentException)
                {
                    // Ignore bogus value
                }
            }
        }
        private JoinTypes _lineJoin;

        public double miterLimit
        {
            get { return _miterLimit; }
            set
            {
                if ((0.0 < value) && !double.IsInfinity(value))
                {
                    _miterLimit = value;
                }
            }
        }
        private double _miterLimit;

        public void clearRect(double x, double y, double w, double h)
        {
            if ((0 < w) && (0 < h))
            {
                if ((x <= 0) && (y <= 0) && (_root.Width <= w) && (_root.Height <= h))
                {
                    // Optimize common scenario of clearing the entire canvas
                    _root.Children.Clear();
                }
                else
                {
                    // Apply the necessary exclusive clip to all children
                    foreach (var canvas in _root.Children.OfType<Canvas>())
                    {
                        var group = new GeometryGroup();
                        group.Children.Add(new RectangleGeometry { Rect = new Rect(0, 0, _root.Width, _root.Height) });
                        group.Children.Add(new RectangleGeometry { Rect = new Rect(x, y, w, h) });
                        canvas.Clip = group;
                    }
                }
                // Create a new canvas to draw on
                _canvas = new Canvas { Width = _root.Width, Height = _root.Height, RenderTransform = _transformGroup };
                _root.Children.Add(_canvas);
            }
        }

        public void fillRect(double x, double y, double w, double h)
        {
            if ((0 < w) && (0 < h))
            {
                var rect = new Rectangle { Width = w, Height = h, Fill = Utility.CloneAndMapGradientBrush(_fill, x, y), Opacity = globalAlpha };
                Canvas.SetLeft(rect, x);
                Canvas.SetTop(rect, y);
                _canvas.Children.Add(rect);
            }
        }

        public void strokeRect(double x, double y, double w, double h)
        {
            var rect = new Rectangle { Width = w, Height = h, Stroke = Utility.CloneAndMapGradientBrush(_stroke, x, y), StrokeThickness = lineWidth, StrokeLineJoin = JoinTypeToPenLineJoin(_lineJoin), StrokeMiterLimit = _miterLimit, Opacity = globalAlpha };
            Canvas.SetLeft(rect, x);
            Canvas.SetTop(rect, y);
            _canvas.Children.Add(rect);
        }

        public void beginPath()
        {
            _pathGeometry = new PathGeometry();
            _pathGeometry.Figures.Add(new PathFigure { StartPoint = new Point() });
            _hasSubPaths = false;
        }

        public void closePath()
        {
            if (_hasSubPaths)
            {
                var figure = _pathGeometry.Figures.Last();
                figure.IsClosed = true;
                figure = new PathFigure { StartPoint = figure.StartPoint };
            }
        }

        public void moveTo(double x, double y)
        {
            var figure = new PathFigure { StartPoint = new Point(x, y) };
            _pathGeometry.Figures.Add(figure);
            _hasSubPaths = true;
        }

        public void lineTo(double x, double y)
        {
            if (!_hasSubPaths)
            {
                moveTo(x, y);
            }
            var figure = _pathGeometry.Figures.Last();
            figure.Segments.Add(new LineSegment { Point = new Point(x, y) });
        }

        public void quadraticCurveTo(double cpx, double cpy, double x, double y)
        {
            if (!_hasSubPaths)
            {
                moveTo(x, y);
            }
            var figure = _pathGeometry.Figures.Last();
            figure.Segments.Add(new QuadraticBezierSegment { Point1 = new Point(cpx, cpy), Point2 = new Point(x, y) });
        }

        public void bezierCurveTo(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y)
        {
            if (!_hasSubPaths)
            {
                moveTo(x, y);
            }
            var figure = _pathGeometry.Figures.Last();
            figure.Segments.Add(new BezierSegment { Point1 = new Point(cp1x, cp1y), Point2 = new Point(cp2x, cp2y), Point3 = new Point(x, y) });
        }

        public void arc(double x, double y, double radius, double startAngle, double endAngle, bool anticlockwise)
        {
            var figure = _pathGeometry.Figures.Last();
            var start = new Point(x + (radius * Math.Cos(startAngle)), y + (radius * Math.Sin(startAngle)));
            if (_hasSubPaths)
            {
                // Draw line from last point of path to start of arc
                figure.Segments.Add(new LineSegment { Point = start });
            }
            else
            {
                // Start at start point
                figure.StartPoint = start;
                _hasSubPaths = true;
            }
            if (2 * Math.PI <= Math.Abs(endAngle - startAngle))
            {
                // Need to draw complete circles as two half-circles
                arc(x, y, radius, startAngle, Math.PI, anticlockwise);
                startAngle = Math.PI;
            }
            // Normalize angles
            start = new Point(x + (radius * Math.Cos(startAngle)), y + (radius * Math.Sin(startAngle)));
            var end = new Point(x + (radius * Math.Cos(endAngle)), y + (radius * Math.Sin(endAngle)));
            if (anticlockwise)
            {
                if (startAngle < endAngle)
                {
                    startAngle += 2 * Math.PI;
                }
            }
            else
            {
                if (endAngle < startAngle)
                {
                    endAngle += 2 * Math.PI;
                }
            }
            // Add arc
            figure.Segments.Add(new ArcSegment { Point = end, Size = new Size(radius, radius), IsLargeArc = Math.PI < Math.Abs(endAngle - startAngle), SweepDirection = anticlockwise ? SweepDirection.Counterclockwise : SweepDirection.Clockwise });
        }

        public void rect(double x, double y, double w, double h)
        {
            moveTo(x, y);
            lineTo(x + w, y);
            lineTo(x + w, y + h);
            lineTo(x, y + h);
            lineTo(x, y);
            moveTo(x, y);
        }

        public void fill()
        {
            _canvas.Children.Add(new Path { Data = Utility.ClonePathGeometry(_pathGeometry), Fill = _fill, Opacity = globalAlpha });
        }

        public void stroke()
        {
            _canvas.Children.Add(new Path { Data = Utility.ClonePathGeometry(_pathGeometry), Stroke = _stroke, StrokeThickness = lineWidth, Opacity = globalAlpha, StrokeStartLineCap = CapTypeToPenLineCap(_lineCap), StrokeEndLineCap = CapTypeToPenLineCap(_lineCap), StrokeLineJoin = JoinTypeToPenLineJoin(_lineJoin), StrokeMiterLimit = miterLimit });
        }

        [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Data is in string form.")]
        public void drawImage(HtmlElement image, double dx, double dy)
        {
            var imageElement = new Image { Source = new BitmapImage(new Uri(HtmlPage.Document.DocumentUri, image.GetAttribute("src"))), Opacity = globalAlpha };
            Canvas.SetLeft(imageElement, dx);
            Canvas.SetTop(imageElement, dy);
            _canvas.Children.Add(imageElement);
        }

        [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Data is in string form.")]
        public void drawImage(HtmlElement image, double dx, double dy, double dw, double dh)
        {
            var imageElement = new Image { Source = new BitmapImage(new Uri(HtmlPage.Document.DocumentUri, image.GetAttribute("src"))), Width = dw, Height = dh, Stretch = Stretch.Fill, Opacity = globalAlpha };
            Canvas.SetLeft(imageElement, dx);
            Canvas.SetTop(imageElement, dy);
            _canvas.Children.Add(imageElement);
        }

        [SuppressMessage("Microsoft.Usage", "CA2234:PassSystemUriObjectsInsteadOfStrings", Justification = "Data is in string form.")]
        public void drawImage(HtmlElement image, double sx, double sy, double sw, double sh, double dx, double dy, double dw, double dh)
        {
            var imageElement = new Image { Stretch = Stretch.Fill, Opacity = globalAlpha };
            imageElement.Clip = new RectangleGeometry();
            _canvas.Children.Add(imageElement);
            imageElement.ImageOpened += delegate
            {
                // Calculate bounds to apply specified bounds
                var xm = dw / sw;
                var ym = dh / sh;
                Canvas.SetLeft(imageElement, dx - (sx * xm));
                Canvas.SetTop(imageElement, dy - (sy * ym));
                imageElement.Width = imageElement.ActualWidth * xm;
                imageElement.Height = imageElement.ActualHeight * ym;
                imageElement.Clip = new RectangleGeometry { Rect = new Rect(sx * xm, sy * ym, dw, dh) };
            };
            imageElement.Source = new BitmapImage(new Uri(HtmlPage.Document.DocumentUri, image.GetAttribute("src")));
        }

        private enum CapTypes { butt, round, square };
        private static PenLineCap CapTypeToPenLineCap(CapTypes capType)
        {
            switch (capType)
            {
                case CapTypes.butt:
                    return PenLineCap.Flat;
                case CapTypes.round:
                    return PenLineCap.Round;
                case CapTypes.square:
                    return PenLineCap.Square;
                default:
                    throw new NotSupportedException();
            }
        }

        private enum JoinTypes { round, bevel, miter };
        private static PenLineJoin JoinTypeToPenLineJoin(JoinTypes joinType)
        {
            switch (joinType)
            {
                case JoinTypes.bevel:
                    return PenLineJoin.Bevel;
                case JoinTypes.miter:
                    return PenLineJoin.Miter;
                case JoinTypes.round:
                    return PenLineJoin.Round;
                default:
                    throw new NotSupportedException();
            }
        }

        // Represents a drawing state
        private class DrawingState
        {
            public object strokeStyle;
            public object fillStyle;
            public double globalAlpha;
            public double lineWidth;
            public string lineCap;
            public string lineJoin;
            public double miterLimit;
            public TransformGroup transformGroup;
        }
    }
}
